package demisemapping;
import com.melloware.jintellitype.HotkeyListener;
import com.melloware.jintellitype.JIntellitype;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.image.*;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;

public class Main extends JFrame implements GameNode {
    public final static int WIDTH = 800;
    public final static int HEIGHT = 800;
    public final static int SCREEN_WIDTH;
    public final static int SCREEN_HEIGHT;
    static {
        Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
        SCREEN_WIDTH = d.width;
        SCREEN_HEIGHT = d.height;
    }
    
    public final static int HOTKEY_QUIT = 1;
    public final static int HOTKEY_PREVIEW_TRAVEL = 2;
    public final static int HOTKEY_TRAVEL = 3;
    public final static int HOTKEY_TRAVEL_HOME = 4;
    public final static int HOTKEY_TELEPORT_DOWN = 5;
    public final static int HOTKEY_DELETE_EDGES = 6;
    public final static int HOTKEY_CAST_SPELLS1 = 7;
    public final static int HOTKEY_CAST_SPELLS2 = 8;
    public final static int HOTKEY_CAST_SPELLS3 = 9;
    public final static int HOTKEY_CAST_SPELLS4 = 10;
    public final static int HOTKEY_ACCUM_SEQ_TOGGLE = 11;
    public final static int HOTKEY_ZOOM_IN = 12;
    public final static int HOTKEY_ZOOM_OUT = 13;
    public final static int HOTKEY_FIGHT = 14;
    public final static int HOTKEY_OPEN = 15;
    
    // wait 100 ms for picture until we assume game window cannot be located
    public final static int NOT_VISIBLE_TIME = 100;
    
    public static Robot r;
    
//    static {
//        try {  } catch (Exception ex) { ex.printStackTrace(); }
//    }
    public static BufferedImage bi;
    ArrayList<ImageNode> nodes = new ArrayList<ImageNode>();
    
    Location current, last, dest;
    char direction;
    int viewingLevel;
    double travelProgress;
    
    Set<PixelSequence> skullPixelSequences;
    boolean fightingMonsters;
    long fightingMonstersTime;
    final long FIGHTING_ENTROPY_NANOS = 1000000000;

    TreeSet<PixelSequence> sequences = new TreeSet<PixelSequence>();
    boolean accumulateSequences;
    
    ArrayList<Location> highlightLocations = new ArrayList<Location>();
    
    long gameFoundTime;
    long mapFoundTime;
    int viewMode;
    
    public Main() throws Exception {
        r = new Robot();
        
        setTitle("Demise Tool ([ALT-ESC] to exit)");
        setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        setLayout(new GridLayout(1, 0));
        
        setBackground(Color.BLACK);
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        setVisible(true);
        pack();
        //int left = 1680, top = 0;
        int left = 1100, top = 0;
        //int left = 840, top = 30;
        setLocation(left, top);
        
        createBufferStrategy(2);
        
        // setup global hotkeys
        JIntellitype jit = JIntellitype.getInstance();
        jit.registerHotKey(HOTKEY_QUIT,
                JIntellitype.MOD_ALT, KeyEvent.VK_ESCAPE);
        jit.registerHotKey(HOTKEY_PREVIEW_TRAVEL,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_X);
        jit.registerHotKey(HOTKEY_TRAVEL,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_Z);
        jit.registerHotKey(HOTKEY_TRAVEL,
                JIntellitype.MOD_CONTROL, KeyEvent.VK_1);
        jit.registerHotKey(HOTKEY_FIGHT,
                JIntellitype.MOD_CONTROL, KeyEvent.VK_2);
        jit.registerHotKey(HOTKEY_OPEN,
                JIntellitype.MOD_CONTROL, KeyEvent.VK_3);
        jit.registerHotKey(HOTKEY_TRAVEL_HOME,
                JIntellitype.MOD_CONTROL, KeyEvent.VK_4);
        jit.registerHotKey(HOTKEY_TRAVEL_HOME,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_H);
        jit.registerHotKey(HOTKEY_TELEPORT_DOWN,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_T);
        jit.registerHotKey(HOTKEY_DELETE_EDGES,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_D);
        jit.registerHotKey(HOTKEY_CAST_SPELLS1,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_1);
        jit.registerHotKey(HOTKEY_CAST_SPELLS2,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_2);
        jit.registerHotKey(HOTKEY_CAST_SPELLS3,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_3);
        jit.registerHotKey(HOTKEY_CAST_SPELLS4,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_4);
        jit.registerHotKey(HOTKEY_ACCUM_SEQ_TOGGLE,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_A);
        jit.registerHotKey(HOTKEY_ZOOM_IN,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_F12);
        jit.registerHotKey(HOTKEY_ZOOM_OUT,
                JIntellitype.MOD_CONTROL+JIntellitype.MOD_SHIFT, KeyEvent.VK_F11);
        
        jit.addHotKeyListener(new HotkeyListener() {
            public void onHotKey(int id) {
                // initialize flags
                Dungeon.setFlag(Dungeon.FLAG_HIGHLIGHTED, false);
                
                // handle particular key press
                switch (id) {
                    case HOTKEY_QUIT:
                        exit(); break;
                    case HOTKEY_PREVIEW_TRAVEL:
                        travelTo((Location) dest.clone(), false);
                        break;
                    case HOTKEY_TRAVEL:
                        travelTo((Location) dest.clone(), true);
                        break;
                    case HOTKEY_TRAVEL_HOME:
                        //travelTo(new Location("24,5,3"), true);
                        travelTo(new Location("24,2,2"), true);
                        //try {
                        //    Thread.sleep(100);
                        //    Macros.tapKey(r, KeyEvent.VK_UP);
                        //} catch (Exception ex) { ex.printStackTrace(); }
                        break;
                    case HOTKEY_TELEPORT_DOWN:
                        try {
                            Thread.sleep(500);
                            teleportDown();
                        } catch (Exception ex) { ex.printStackTrace(); }
                        break;
                    case HOTKEY_DELETE_EDGES:
                        Dungeon.deleteEdges((Location) current.clone());
                        break;
                    case HOTKEY_CAST_SPELLS1:
                        System.out.println("Casting spell set 1...");
                        try {
                            Thread.sleep(500);
                            Macros.castSpells1(r);
                        } catch (Exception ex) { ex.printStackTrace(); }
                        break;
                    case HOTKEY_CAST_SPELLS2:
                        System.out.println("Casting spell set 2...");
                        try {
                            Thread.sleep(500);
                            Macros.castSpells2(r);
                        } catch (Exception ex) { ex.printStackTrace(); }
                        break;
                    case HOTKEY_CAST_SPELLS3:
                        System.out.println("Casting spell set 3...");
                        try {
                            Thread.sleep(500);
                            Macros.castSpells3(r);
                        } catch (Exception ex) { ex.printStackTrace(); }
                        break;
                    case HOTKEY_CAST_SPELLS4:
                        System.out.println("Casting spell set 4...");
                        try {
                            Thread.sleep(500);
                            Macros.castSpells4(r);
                        } catch (Exception ex) { ex.printStackTrace(); }
                        break;
                    case HOTKEY_ACCUM_SEQ_TOGGLE:
                        accumulateSequences = !accumulateSequences;
                        System.out.println((accumulateSequences?
                            "Accumulating":"Not accumulating")+" sequences.");
                        if (!accumulateSequences) {
                            for (PixelSequence ps : sequences) {
                                System.out.println(ps);
                            }
                            sequences.clear();
                        }
                        break;
                    case HOTKEY_ZOOM_IN:
                        Dungeon.TILESIZE++;
                        Images.loadImages();
                        repaint();
                        break;
                    case HOTKEY_ZOOM_OUT:
                        if (Dungeon.TILESIZE > 1) {
                            Dungeon.TILESIZE--;
                            Images.loadImages();
                            repaint();
                        }
                        break;
                    case HOTKEY_FIGHT:
                        System.out.println("Pressing f...");
                        try {
                            Thread.sleep(100);
                            Macros.type(r, "f");
                        } catch (Exception ex) { ex.printStackTrace(); }
                        break;
                    case HOTKEY_OPEN:
                        System.out.println("Pressing o...");
                        try {
                            Thread.sleep(100);
                            Macros.type(r, "o");
                        } catch (Exception ex) { ex.printStackTrace(); }
                        break;
                    default: break;
                }
            }
        });
        
        // read in pixel sequences for identifying "fighting monsters" skull
        skullPixelSequences = new TreeSet<PixelSequence>();
        String[] fileSuffixes = {"win", "win_wide", "win_full", "fs", "fs_wide", "fs_full"};
        for (String suf : fileSuffixes) {
            Scanner fin = new Scanner(new File("data/skull_pixelsequences_" + suf + ".txt"));
            while (fin.hasNextLine()) {
                skullPixelSequences.add(new PixelSequence(fin.nextLine()));
            }
        }
        
        // start screen capture thread
        Anim a = new Anim();
        a.start();
        
        GameLocator locator = new GameLocator();
        locator.nodes.add(this);
        nodes.add(locator);
        
//        PreviewJFrame prev1 = new PreviewJFrame(0);
//        PreviewJFrame prev2 = new PreviewJFrame(1);
//        locator.nodes.add(prev1);
//        locator.nodes.add(prev2);
        
        // start dungeon auto-save thread
        Autosave auto = new Autosave();
        auto.start();
        
        ReachabilityScanner scanner = new ReachabilityScanner();
        scanner.start();
    }
    
    public void exit() {
        try {
            Dungeon.save();
        } catch (Exception ex) { ex.printStackTrace(); }
        System.exit(0);
    }
    
    public LinkedList<Integer> getKeyCodes(char dir, Location a, Location b) {
        int offsetX = b.j-a.j, offsetY = b.i-a.i;
        assert(Math.abs(offsetX)+Math.abs(offsetY) == 1);
        LinkedList<Integer> result = new LinkedList<Integer>();
        if (dir == 'N') {
            if (offsetX == -1) result.add(KeyEvent.VK_LEFT);
            else if (offsetX == 1) result.add(KeyEvent.VK_RIGHT);
            else if (offsetY == -1) for (int i=0;i<2;i++) result.add(KeyEvent.VK_LEFT);
        } else if (dir == 'S') {
            if (offsetX == -1) result.add(KeyEvent.VK_RIGHT);
            else if (offsetX == 1) result.add(KeyEvent.VK_LEFT);
            else if (offsetY == 1) for (int i=0;i<2;i++) result.add(KeyEvent.VK_LEFT);
        } else if (dir == 'W') {
            if (offsetX == 1) for (int i=0;i<2;i++) result.add(KeyEvent.VK_LEFT);
            else if (offsetY == -1) result.add(KeyEvent.VK_LEFT);
            else if (offsetY == 1) result.add(KeyEvent.VK_RIGHT);
        } else if (dir == 'E') {
            if (offsetX == -1) for (int i=0;i<2;i++) result.add(KeyEvent.VK_LEFT);
            else if (offsetY == -1) result.add(KeyEvent.VK_RIGHT);
            else if (offsetY == 1) result.add(KeyEvent.VK_LEFT);
        }
        result.add(KeyEvent.VK_UP);
        return result;
    }
    
    public void teleportDown() throws Exception {
        if (direction != 'N') return;
        if (System.currentTimeMillis() - mapFoundTime > NOT_VISIBLE_TIME) return;
        if (current.equals(new Location("24,3,2"))) {
            Macros.teleport(r, new Location("0,11,4"));
        } else if (current.equals(new Location("24,14,6"))) {
            Macros.teleport(r, new Location("0,10,4"));
        } else if (current.equals(new Location("24,24,10"))) {
            Macros.teleport(r, new Location("-5,-14,4"));
        } else if (current.equals(new Location("19,10,14"))) {
            Macros.teleport(r, new Location("5,11,4"));
        } else if (current.equals(new Location("24,21,18"))) {
            Macros.teleport(r, new Location("-2,2,3"));
        }
    }
    
    public void travelTo(Location x, boolean move) {
        // get path to x or error out
        if (x == null) {
            System.out.println("No location selected!");
            return;
        }
        ArrayList<Location> positions = Dungeon.bfsPath(current, x);
        System.out.println("PATH " + current + " --> " + x + ":");
        if (positions == null) {
            System.out.println("No path known!");
            return;
        }
        for (int i=0;i<positions.size();i++) {
            Dungeon.setFlag(Dungeon.FLAG_HIGHLIGHTED, positions);
            System.out.println(positions.get(i));
        }
        // move to location
        if (move) {
            Player player = new Player(positions);
            player.start();
//            while (!player.done) {
//                Thread.yield();
//            }
        }
    }
    
    public void notifyNodes() {
        for (ImageNode x : nodes) x.notifyReady();
    }
    
    public void capture() {
        notifyNodes();
    }
    
    @Override
    public void paint(Graphics _g) {
        //super.paint(g);
        Graphics2D g = (Graphics2D) _g;
        
        // prepare back buffer for double buffered drawing
        BufferedImage backbuffer = new BufferedImage(
                getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D gg = backbuffer.createGraphics();
        gg.clearRect(0, 0, backbuffer.getWidth(), backbuffer.getHeight());

        long now = System.currentTimeMillis();

        // if game window not found recently, draw message
        if (now - gameFoundTime > NOT_VISIBLE_TIME) {
            gg.setColor(Color.YELLOW);
            gg.drawString("Game window could not be located.  To fix this, please:", 10, 40);
            gg.drawString("(1) Place the game window near the top-left corner of the screen", 40, 55);
            gg.drawString("(2) Ensure your are not at the main menu; you must be in town or the dungeon", 40, 70);
            gg.drawString("(3) If in the dungeon, switch to the view wherein the 3D viewport is smallest", 40, 85);
            
        // if map tab was not active recently, draw message
        } else if (now - mapFoundTime > NOT_VISIBLE_TIME) {
            gg.setColor(Color.YELLOW);
            gg.drawString("Minimap could not be located.  To fix this, please:", 10, 40);
            gg.drawString("(1) Ensure you are in the dungeon", 40, 55);
            gg.drawString("(2) Activate the minimap so that it displays " +
                          "at the bottom of the screen.", 40, 70);
            
        // else, do the magic.
        } else {
            
            // draw onto back buffer
            gg.translate(getWidth()/2 - Dungeon.getX(current), getHeight()/2 - Dungeon.getY(current));
            Dungeon.drawLevel(gg, this, viewingLevel);
            //System.out.println("getwidth=" + getWidth() + " backbuffer.width=" + backbuffer.getWidth() + " levelbuffer.width=" + levelbuffer.getWidth());
            
            if (current != null && current.k == viewingLevel) {
                gg.drawImage((direction=='N' ? Images.ARROW_NORTH
                            : direction=='S'? Images.ARROW_SOUTH
                            : direction=='W' ? Images.ARROW_WEST
                            : Images.ARROW_EAST),
                        Dungeon.getX(current)+1,
                        Dungeon.getY(current)+1,
                        this);
//                gg.setColor(Color.YELLOW);
//                gg.fillOval(Dungeon.getX(current)+1, Dungeon.getY(current)+1,
//                            (int) Math.round(Dungeon.TILESIZE*3./4),
//                            (int) Math.round(Dungeon.TILESIZE*3./4));
            }
            
            if (dest != null && dest.k == viewingLevel) {
                gg.setColor(Color.RED);
                gg.fillOval(Dungeon.getX(dest)+1, Dungeon.getY(dest)+1,
                            (int) Math.round(Dungeon.TILESIZE*3./4),
                            (int) Math.round(Dungeon.TILESIZE*3./4));
            }
            gg.translate(-(getWidth()/2 - Dungeon.getX(current)), -(getHeight()/2 - Dungeon.getY(current)));
            
            gg.setColor(Color.WHITE);
            int textY = getHeight()-200; //Dungeon.getY(0);
            gg.drawString("Direction: "+direction, 10, textY+30);
            gg.drawString("Position: "+current, 10, textY+45);
            gg.drawString("Viewing level "+viewingLevel, 10, textY+60);
            gg.drawString((fightingMonsters?"Fighting":"Not fighting")+" monsters.", 10, textY+75);
            gg.drawString("Trip Progress: "+Math.round(travelProgress*100)+"%", 10, textY+100);
            if (!Dungeon.reachable(2, 3, 24)) {
                gg.setColor(Color.YELLOW);
                gg.drawString("Warning: no known path to town.", 10, textY+115);
            }
            gg.setColor(Color.BLACK);
            
        }
        // draw buffer onto canvas
        //System.out.println("insets=" + getInsets().left + " " + getInsets().top);
        g.drawImage(backbuffer,
                    (getWidth()-backbuffer.getWidth())/2+getInsets().left,
                    (getHeight()-backbuffer.getHeight())/2+getInsets().top,
                    this);

    }
    
    public void notifyOfGame(int mode, ArrayList<BufferedImage> images, ArrayList<int[]> shots, int[] w, int[] h) {
        assert(shots.size() == 2);
        viewMode = mode;
        processLocationInfo(shots.get(0), w[0], h[0]);
        processMonsterStatus(shots.get(1), w[1], h[1]);
        gameFoundTime = System.currentTimeMillis();
    }
    
    private void printGrid(boolean[][] grid, int w, int h) {
        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                System.out.print(grid[i][j] ? "#" : " ");
            }
            System.out.println();
        }
    }
    
    String readLine(int color, int[] px, int w, int textX, int textY, int textW, int textH) {
        /* retrieve black pixels in text */
        int x = textX, y = textY;
        boolean[][] black = new boolean[textH][textW];
        int top = -1;
        for (int i = 0; i < textH; i++) {
            for (int j = 0; j < textW; j++) {
                int k = ((y+i)*w+(x+j))*3;
//                System.out.println(px[k] + px[k+1] + px[k+2]);
                if (px[k] + px[k+1] + px[k+2] == color) {
                    black[i][j] = true;
                    if (top == -1) top = i;
                }
            }
        }
        //printGrid(black, textW, textH);
        
        /* retrieve text from black pixels */
        return bfsString(black, top);
    }
    
    public String getCurrentLocation(int[] px, int w) {
        if (viewMode == GameLocator.MODE_FULLSCREEN_FULL || viewMode == GameLocator.MODE_WINDOWED_FULL) {
            String loc;
            if (viewMode == GameLocator.MODE_WINDOWED_FULL) {
                loc = readLine(687, px, w, 0, 0, w, 12);
            } else {
                loc = readLine(693, px, w, 0, 0, w, 12);
            }
            
            char dir = 0;
            for (char c : loc.toCharArray()) if ("NSEW".contains(c+"")) dir = c;
            if (!"NSEW".contains(dir+"")) {
                System.out.println("WARNING: dir " + dir + " is not in NSEW!");
                return "";
            }
            
            return loc.substring(0, loc.indexOf(dir));
        } else {
            String loc = readLine(0, px, w, 13, 0, 45, 12);
            if (loc.contains(",")) return loc;
            loc = readLine(0, px, w, 0, 14, 45, 12);
            return loc;
        }
    }
    
    public String getDestinationLocation(int[] px, int w) {
        if (viewMode == GameLocator.MODE_FULLSCREEN_FULL || viewMode == GameLocator.MODE_WINDOWED_FULL) {
            return "24,2,2"; // home
        } else {
            String dst = readLine(0, px, w, 0, 48, 45, 12);
            return dst;
        }
    }
    
    public String getDirection(int[] px, int w) {
        
        if (viewMode == GameLocator.MODE_FULLSCREEN_FULL || viewMode == GameLocator.MODE_WINDOWED_FULL) {
            String loc;
            if (viewMode == GameLocator.MODE_WINDOWED_FULL) {
                loc = readLine(687, px, w, 0, 0, w, 12);
            } else {
                loc = readLine(693, px, w, 0, 0, w, 12);
            }
            
            char dir = 0;
            for (char c : loc.toCharArray()) if ("NSEW".contains(c+"")) dir = c;
            if (!"NSEW".contains(dir+"")) {
                System.out.println("WARNING: dir " + dir + " is not in NSEW!");
                return "";
            }
            
            return dir+"";
        } else {
            String dir = readLine(0, px, w, 0, 14, 45, 12);
            if (dir.equals("W") || dir.equals("N") || dir.equals("E") || dir.equals("S")) {
                return dir;
            }
            dir = readLine(0, px, w, 0, 26, 45, 12);
            return dir;
        }
    }
    
    public int getLevelNumber(int[] px, int w) {
        String level = "";
        
        if (viewMode == GameLocator.MODE_FULLSCREEN_FULL || viewMode == GameLocator.MODE_WINDOWED_FULL) {
            String loc;
            if (viewMode == GameLocator.MODE_WINDOWED_FULL) {
                loc = readLine(687, px, w, 0, 0, w, 12);
            } else {
                loc = readLine(693, px, w, 0, 0, w, 12);
            }
            
            char dir = 0;
            for (char c : loc.toCharArray()) if ("NSEW".contains(c+"")) dir = c;
            if (!"NSEW".contains(dir+"")) {
                System.out.println("WARNING: dir " + dir + " is not in NSEW!");
                return -1;
            }
            
            level = loc.substring(loc.indexOf(dir)+1);
        } else {
            level = readLine(0, px, w, 33, 94, 12, 12);
        }
        
        int result = -1;
        if (level.length() >= 1 && level.length() <= 2) {
            result = Integer.parseInt(level);
        }
        return result;
    }
    
    public void processLocationInfo(int[] px, int w, int h) {
        String loc = getCurrentLocation(px, w);
        if (loc != null && !loc.equals("")) {
            last = current;
            current = new Location(loc);
            if (current.k == -1) current = null;
            if (current != null) {
                mapFoundTime = System.currentTimeMillis();
                Dungeon.mapTile(current, last);
            }
        }
        String dst = getDestinationLocation(px, w);
        if (dst != null && !dst.equals("")) dest = new Location(dst);

        String dir = getDirection(px, w);
        if (dir != null && !dir.equals("")) direction = dir.charAt(0);
        
        int level = getLevelNumber(px, w);
        if (level != -1) viewingLevel = level;
    }
    
    public void processMonsterStatus(int[] px, int w, int h) {
        PixelSequence ps = new PixelSequence();
        for (int i=0;i<9;i+=3) {
            ps.a.add(new Location(px[i]+","+px[i+1]+","+px[i+2]));
        }
        if (accumulateSequences && sequences.add(ps)) {
            System.out.println("sequences.size() = " + sequences.size());
        }
        
        boolean fighting = skullPixelSequences.contains(ps);
        if (fighting) {
            fightingMonsters = true;
            fightingMonstersTime = System.nanoTime();
        } else {
            if (fightingMonsters && System.nanoTime() > fightingMonstersTime + FIGHTING_ENTROPY_NANOS) {
                fightingMonsters = false;
            }
        }
    }
    
    int[] adji = {-1,-1,-1,+0,+0,+1,+1,+1};
    int[] adjj = {-1,+0,+1,-1,+1,-1,+0,+1};
    
    String bfsString(boolean grid[][], int top) {
        assert(grid.length > 0);
        boolean[][] visited = new boolean[grid.length][grid[0].length];
        String result = "";
        
        int charNumber = 0;
        for (int j=0;j<grid[0].length;j++) {
            for (int i=0;i<grid.length;i++) {
                if (grid[i][j] && !visited[i][j]) {
                    boolean unidentified = false;
                    
                    LinkedList<Integer> q = new LinkedList<Integer>();
                    q.add(i);
                    q.add(j);
                    visited[i][j] = true;
                    int metric = 0;
                    
                    while (q.size() > 0) {
                        int ii = q.remove();
                        int jj = q.remove();
                        
                        for (int k=0;k<adji.length;k++) {
                            int ai = ii+adji[k];
                            int aj = jj+adjj[k];
                            
                            if (ai < 0 || ai >= grid.length ||
                                aj < 0 || aj >= grid[0].length ||
                                !grid[ai][aj] || visited[ai][aj]) continue;
                            
                            visited[ai][aj] = true;
                            q.add(ai);
                            q.add(aj);
                            int dist = ai-top;
                            metric += dist*dist*dist;
                        }
                    }
                    //System.out.print("character " + charNumber + " has metric " + metric);
                    
                    switch(metric) {
                        case 1297: case 785:    result += "1"; break;
                        case 3344: case 2156:   result += "2"; break;
                        case 2727: case 1713:   result += "3"; break;
                        case 2260: case 1320:   result += "4"; break;
                        case 2808: case 1759:   result += "5"; break;
                        case 3196: case 1956:   result += "6"; break;
                        case 1296: case 784:    result += "7"; break;
                        case 3167: case 1937:   result += "8"; break;
                        case 2890: case 1913:   result += "9"; break;
                        case 3103: case 1910:   result += "0"; break;
                        case 512:  case 1241:   result += ","; break;
                        case 3376: case 2009:   result += "N"; break;
                        case 2018: case 6397:   result += "W"; break; // note: 6397 is actually a ligature "(W"
                        case 2791: case 2120:   result += "S"; break;
                        case 3536: case 2264:   result += "E"; break;
                        case 2754:              break; // '[' and ']'
                        case 3024: case 3595: case 2169: case 1322: case 3025: break; // "(ast)"
                        case 3049: case 1287: case 2674: case 3402: case 3208: break; // "orhue"
                        default:
                            // here, we ignore any metrics >= 4000, because they will
                            // represent large ligatures or pieces of the user interface...
                            if (metric < 4000) unidentified = true;
                            break;
                    }
                    
                    //System.out.println((unidentified ? " [UNIDENTIFIED]" : ""));
                    if (unidentified) {
                        printGrid(grid, grid[0].length, grid.length);
                        System.out.println("character " + charNumber + " has unidentified metric " + metric);
                    }
                    
                    charNumber++;
                }
            }
        }
        
        return result;
    }
    
    public void fightMonsters() {
        // do nothing for now -- later, may cast spells etc.
    }
    
    public static void main(String[] args) throws Exception {
        new Main();
    }
    
    private class Anim extends Thread {
        @Override
        public void run() {
            int i = 0;
            while (true) {
                try {
                    sleep(30);
                    capture();
                    repaint();
                } catch (InterruptedException ex) {
                    Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }
    
    private class Autosave extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(5*60*1000);
                    Dungeon.save();
                } catch (Exception ex) { ex.printStackTrace(); }
            }
        }
    }
    
    private class ReachabilityScanner extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(1000);
                    if (current != null) {
                        Dungeon.bfsReachable(current);
                    }
                } catch (Exception ex) { ex.printStackTrace(); }
            }
        }
    }
    
    private class Player extends Thread {
        public volatile boolean done = false;
        
        ArrayList<Location> positions;
        public Player(ArrayList<Location> positions) {
            this.positions = positions;
        }
        
        @Override
        public void run() {
            try {
                int retries = 50;
                Thread.sleep(500);
                final int MOVE_DELAY = 30;
                for (int i=1;i<positions.size();i++) {
                    int j = 0;
                    for (;j<retries;j++) {
                        if (current.equals(positions.get(i-1))) break;
//                        System.out.println("Retry #"+j+": Expected "+positions.get(i-1));
                        Thread.sleep(MOVE_DELAY);
                    }
                    if (j == retries) {
                        System.out.println("Giving up!");
                        break;
                    }
                    
                    LinkedList<Integer> keycodes = getKeyCodes(direction, current, positions.get(i));
                    for (Integer key : keycodes) {
                        while (fightingMonsters) Thread.sleep(100);
//                        r.keyPress(KeyEvent.VK_A);
//                        r.delay(100);
//                        r.keyRelease(KeyEvent.VK_A);
                        r.keyPress(key);
//                        System.out.println("pressing " + key);
                        Thread.sleep(10);
                        r.keyRelease(key);
//                        System.out.println("releasing " + key);
                    }
                    travelProgress = i / (positions.size()-1.0);
                    Thread.sleep(MOVE_DELAY);
                }
            } catch (Exception ex) { ex.printStackTrace(); }
            finally {
                done = true;
            }
        }
    }
    
}
